﻿using System;
using System.Linq;
using System.Reflection;
using DesignByContract;
using DesignByContract.Exceptions;

namespace TestHelper
{
    public static class ObjectExtensions
    {
        /// <summary>
        /// Basic binding flags for most methods.
        /// </summary>
        private const BindingFlags FLAGS = BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Static;

        /// <summary>
        /// Creates property level copy of any concrete class.  
        /// Searches public and protected properties and ignores properties without setters and getters.
        /// </summary>
        /// <typeparam name="T">The concrete type of the object</typeparam>
        /// <param name="source">The object to copy.</param>
        /// <returns>A copied object</returns>
        public static T Copy<T>(this T source) where T : class
        {
            T copy = Activator.CreateInstance(typeof(T)) as T;
            source.CopyTo(copy);
            return copy;
        }

        /// <summary>
        /// Updates the properties of an object with values of another object.
        /// Searches public and protected properties and ignores properties without setters and getters.
        /// </summary>
        /// <typeparam name="T">The concrete type of the object</typeparam>
        /// <param name="objectToUpdate">The object to update</param>
        /// <param name="source">The object whose values will be used</param>
        public static void CopyTo<T>(this T source, T objectToUpdate) where T : class
        {
            //Get all the properties that have a setter and getter and copy them.
            var properties = typeof(T).GetProperties(FLAGS).Where(p => p.GetSetMethod(true) != null && p.GetGetMethod(true) != null);
            foreach (var property in properties)
            {
                property.SetValue(objectToUpdate, property.GetValue(source, null), null);
            }
        }

        /// <summary>
        /// Returns a field given the name and the type of object.
        /// </summary>
        /// <param name="fieldName">The name of the field</param>
        /// <param name="objectType">The type of object to which the field belongs</param>
        /// <returns>The field</returns>
        private static FieldInfo GetField(string fieldName, Type objectType)
        {
            FieldInfo field = objectType.GetField(fieldName, FLAGS);
            if (field == null)
            {
                Type baseType = objectType.BaseType;
                while (baseType != typeof(object))
                {
                    field = GetField(fieldName, baseType);
                    if (field != null)
                    {
                        break;
                    }
                    baseType = baseType.BaseType;
                }
            }
            return field;
        }

        /// <summary>
        /// Returns a property given the name and the type of object.
        /// </summary>
        /// <param name="propertyName">The name of the property</param>
        /// <param name="objectType">The type of object to which the property belongs</param>
        /// <returns>The property</returns>
        private static PropertyInfo GetProperty(string propertyName, Type objectType)
        {
            PropertyInfo property = objectType.GetProperty(propertyName, FLAGS);
            if (property == null)
            {
                Type baseType = objectType.BaseType;
                while (baseType != typeof(object))
                {
                    property = GetProperty(propertyName, baseType);
                    if (property != null)
                    {
                        break;
                    }
                    baseType = baseType.BaseType;
                }
            }
            return property;
        }

        /// <summary>
        /// Gets the value of a field for an object.
        /// </summary>
        /// <typeparam name="TProperty">The type of the field to look for.</typeparam>
        /// <param name="propertyName">The name of the field.</param>
        /// <param name="source">>The object to which the field belongs</param>
        /// <returns>The value of the field</returns>
        public static TProperty PropertyValue<TProperty>(this object source, string propertyName)
        {
            Check.Require(source != null, "Cannot get a property value for a null object.");
            PropertyInfo property = GetProperty(propertyName, source.GetType());

            Check.Ensure(property != null,
                "The property '" + propertyName + "' was not found in " + source.GetType().FullName);
            return (TProperty)property.GetValue(source, FLAGS, null, null, null);
        }

        /// <summary>
        /// Sets the value of a property for an object.
        /// </summary>
        /// <typeparam name="TProperty">The type of the property to look for.</typeparam>
        /// <param name="propertyName">The name of the property.</param>
        /// <param name="source">>The object to which the property belongs</param>
        /// <param name="value">The value to which the property will be set.</param>
        public static void SetPropertyValue<TProperty>(this object source, string propertyName, TProperty value)
        {
            Check.Require(source != null, "Cannot set a property value for a null object.");
            PropertyInfo property = GetProperty(propertyName, source.GetType());

            Check.Ensure(property != null,
                "The property '" + propertyName + "' was not found in " + source.GetType().FullName);
            property.SetValue(source, value, FLAGS, null, null, null);
        }

        /// <summary>
        /// Gets the value of a field for an object.
        /// </summary>
        /// <typeparam name="TField">The type of the field to look for.</typeparam>
        /// <param name="fieldName">The name of the field.</param>
        /// <param name="source">>The object to which the field belongs</param>
        /// <returns>The value of the field</returns>
        public static TField FieldValueEx<TField>(this object source, string fieldName)
        {
            Check.Require(source != null, "Cannot get a field value for a null object.");
            FieldInfo field = GetField(fieldName, source.GetType());

            Check.Ensure(field != null,
                "The field '" + fieldName + "' was not found in " + source.GetType().FullName);
            return (TField)field.GetValue(source);
        }

        /// <summary>
        /// Gets the value of a field for an object.
        /// </summary>
        /// <typeparam name="TField">The type of the field to look for.</typeparam>
        /// <param name="fieldName">The name of the field.</param>
        /// <param name="source">>The object to which the field belongs</param>
        /// <param name="value">The value to which the field will be set.</param>
        public static void SetFieldValue<TField>(this object source, string fieldName, TField value)
        {
            Check.Require(source != null, "Cannot set a field value for a null object.");
            FieldInfo field = GetField(fieldName, source.GetType());

            Check.Ensure(field != null,
                "The field '" + fieldName + "' was not found in " + source.GetType().FullName);
            field.SetValue(source, value);
        }

        /// <summary>
        /// Gets a method for the specified object given a method name and parameter types.
        /// </summary>
        /// <param name="source">The <see cref="Type"/> to which the method belongs.</param>
        /// <param name="methodName">The name of the method</param>
        /// <param name="parameterTypes">The types of the parameters in the same 
        /// order that they appear in the method signature.  If there are not parameters then pass null.</param>
        /// <exception cref="DesignByContractException">
        /// Thrown if the <paramref name="source"/> is null or if the <paramref name="methodName"/> is null.
        /// </exception>
        /// <returns>The <see cref="MethodInfo"/></returns>
        public static MethodInfo GetMethod(this Type source, String methodName, Type[] parameterTypes)
        {
            Check.Require(source != null, "Cannot find a method for a null type");
            Check.Require(!String.IsNullOrEmpty(methodName), "The method name cannot be null or empty");
            MethodInfo foundMethod = null;
            if (parameterTypes != null)
            {
                MethodInfo[] methods = source.GetMethods(FLAGS);
                foreach (MethodInfo method in methods)
                {
                    //we assume that its a match until set otherwise.
                    bool matchFound = true;
                    ParameterInfo[] parameters = method.GetParameters();
                    //make sure we are looking at the right method and overload
                    if (method.Name.Equals(methodName) &&
                        parameters.Length == parameterTypes.Length)
                    {
                        //Let's move through the parameters in order
                        for (int i = 0; i < parameters.Length; i++)
                        {
                            //if the parameter type is an object or the types match, then no need to search 
                            if (parameters[i].ParameterType !=
                                typeof(object))
                            {
                                if (parameters[i].ParameterType !=
                                    parameterTypes[i])
                                {
                                    //if its an interface, then we have to see if the parameters specified implement it
                                    if (parameters[i].ParameterType.IsInterface)
                                    {
                                        var local = i;
                                        matchFound =
                                            parameterTypes[i].GetInterfaces()
                                                .Where(it => parameters[local].ParameterType.Equals(it))
                                                .Any();
                                    }
                                    else
                                    {
                                        bool extendsSuperClass = false;
                                        //let's look at base types until we reach object
                                        Type baseType = parameterTypes[i].BaseType;
                                        while (baseType != typeof(object))
                                        {
                                            if (parameters[i].ParameterType == baseType)
                                            {
                                                extendsSuperClass = true;
                                                break;
                                            }
                                            baseType = baseType.BaseType;
                                        }

                                        matchFound = extendsSuperClass;
                                    }
                                }
                            }
                            if (matchFound == false)
                            {
                                break;
                            }
                        }
                    }
                    else
                    {
                        matchFound = false;
                    }

                    if (matchFound)
                    {
                        foundMethod = method;
                    }
                }
            }
            else
            {
                foundMethod = source.GetType().GetMethod(methodName, FLAGS);
            }
            Check.Ensure(foundMethod != null, methodName + " was not found in " + source.GetType().FullName +
                " with the specified parameters.  Please make sure you passed the parameters in the correct order.");
            return foundMethod;
        }

        /// <summary>
        /// Gets a method for the specified object given a method name and parameter types.
        /// </summary>
        /// <param name="source">The object to which the method belongs.</param>
        /// <param name="methodName">The name of the method</param>
        /// <param name="parameterTypes">The types of the parameters in the same 
        /// order that they appear in the method signature.  If there are not parameters then pass null.</param>
        /// <returns>The <see cref="MethodInfo"/></returns>
        public static MethodInfo GetMethod(this object source, string methodName, Type[] parameterTypes)
        {
            Check.Require(source != null, "Cannot find a method for a null object");
            return GetMethod(source.GetType(), methodName, parameterTypes);
        }

        /// <summary>
        /// Gets a method for the specified object given a method name.
        /// </summary>
        /// <param name="source">The object to which the method belongs.</param>
        /// <param name="methodName">The name of the method</param>
        /// <returns>The <see cref="MethodInfo"/></returns>
        public static MethodInfo GetMethod(this object source, string methodName)
        {
            return source.GetMethod(methodName, null);
        }

        public static TAttribute PropertyAttribute<TAttribute>(this object source, string propertyName)
        {
            Check.Require(source != null, "Cannot get a property value for a null object.");
            var property = GetProperty(propertyName, source.GetType());
            Check.Ensure(property != null,
                "The property '" + propertyName + "' was not found in " + source.GetType().FullName);

            return property.GetCustomAttributes(typeof(TAttribute), true)
                .Cast<TAttribute>()
                .FirstOrDefault();
        }

        public static TAttribute ClassAttribute<TAttribute>(this object source)
        {
            Check.Require(source != null, "Cannot get an attribute value for a null object.");

            return ClassAttribute<TAttribute>(source.GetType());
        }

        public static TAttribute ClassAttribute<TAttribute>(this Type source)
        {
            return source.GetCustomAttributes(typeof(TAttribute), true)
                .Cast<TAttribute>()
                .FirstOrDefault();
        }
    }
}
